////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.charts.series
{

import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.geom.Rectangle;
import mx.charts.HitData;
import mx.charts.chartClasses.BoundedValue;
import mx.charts.chartClasses.DataDescription;
import mx.charts.chartClasses.InstanceCache;
import mx.charts.chartClasses.LegendData;
import mx.charts.chartClasses.Series;
import mx.charts.renderers.DiamondItemRenderer;
import mx.charts.series.items.PlotSeriesItem;
import mx.charts.series.renderData.PlotSeriesRenderData;
import mx.charts.styles.HaloDefaults;
import mx.collections.CursorBookmark;
import mx.core.ClassFactory;
import mx.core.IDataRenderer;
import mx.core.IFactory;
import mx.core.IFlexDisplayObject;
import mx.core.mx_internal;
import mx.graphics.IFill;
import mx.graphics.IStroke;
import mx.graphics.SolidColor;
import mx.styles.CSSStyleDeclaration;
import mx.styles.ISimpleStyleClient;
import mx.charts.chartClasses.CartesianTransform;

use namespace mx_internal;

include "../styles/metadata/FillStrokeStyles.as"
include "../styles/metadata/ItemRendererStyles.as"

/** 
 *  Specifies the radius, in pixels, of the chart element at each data point. 
 *  By default, the PlotChart control draws a circle at each data point.  
 *  You can set this property in MXML or using styles. 
 *  The default value is <code>4</code> pixels. 
 */
[Style(name="radius", type="Number", format="Length", inherit="no")]

/**
 *  Defines a data series for a PlotChart control.
 *  The default itemRenderer for a PlotChart control
 *  is the DiamondRenderer class. 
 *  Optionally, you can define an itemRenderer for the data series.
 *  The itemRenderer must implement the IDataRenderer interface. 
 *
 *  @mxml
 *
 *  <p>The <code>&lt;mx:PlotSeries&gt;</code> tag inherits all the properties
 *  of its parent classes, and adds the following properties:</p>
 *  
 *  <pre>
 *  &lt;mx:PlotSeries
 *    <strong>Properties</strong>
 *    radius="4"
 *    xField="null"
 *    yField="null"
 * 
 *    <strong>Styles</strong>
 *    fill="0xFFFFFF"
 *    itemRenderer="<i>itemRenderer</i>"
 *    legendMarkerRenderer="<i>Defaults to series's itemRenderer</i>"
 *    radius="4"
 *    stroke="<i>IStroke; no default</i>" 
 *  /&gt;
 *  </pre>
 *  </p>
 *  
 *  @see mx.charts.PlotChart
 *  
 *  @includeExample ../examples/PlotChartExample.mxml
 *  
 */
public class PlotSeries extends Series
{
    include "../../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Class initialization
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private static var stylesInited:Boolean = initStyles();
		
	/**
	 *  @private
	 */
	private static function initStyles():Boolean
	{
		HaloDefaults.init();

		var seriesStyle:CSSStyleDeclaration =
			HaloDefaults.createSelector("PlotSeries");		

		seriesStyle.defaultFactory = function():void
		{
			this.fill = new SolidColor(0x4444AA);
			this.itemRenderer = new ClassFactory(DiamondItemRenderer);
			this.radius = 5;
		}

		return true;
	}

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

	/**
	 *  Constructor
	 */
	public function PlotSeries()
	{
		super();

		_instanceCache = new InstanceCache(null, this);
		_instanceCache.creationCallback = applyItemRendererProperties;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  @private
	 */
	private var _instanceCache:InstanceCache;

	/**
	 *  @private
	 */
	private var _renderData:PlotSeriesRenderData;	
	
	//--------------------------------------------------------------------------
	//
	//  Overridden properties
	//
	//--------------------------------------------------------------------------

	//----------------------------------
	//  legendData
	//----------------------------------

	/**
	 *  @private
	 */
	override public function get legendData():Array
	{
		var ld:LegendData = new LegendData();
		var marker:IFlexDisplayObject;
		
		ld.element = this;
		var markerFactory:IFactory = getStyle("legendMarkerRenderer");
		if (markerFactory == null)
			markerFactory = getStyle("itemRenderer");
		if (markerFactory) 
		{
			marker = markerFactory.newInstance();
			if (marker as ISimpleStyleClient)
				(marker as ISimpleStyleClient).styleName = this;
		}

		ld.marker = marker;
		ld.aspectRatio = 1;
		ld.label = displayName;	

		return [ ld ];
	}

	//----------------------------------
	//  renderData
	//----------------------------------

	/**
	 *  The subtype of ChartRenderData used by this series
	 *  to store all data necessary to render.
	 *  Subclasses can override and return a more specialized class
	 *  if they need to store additional information for rendering.
	 */
	override protected function get renderData():Object
	{
		if (!_renderData)
		{
			var renderDataType:Class = this.renderDataType;
			return new renderDataType([], [], 0);
		}

		_renderData.radius = getStyle("radius");			
		return _renderData;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Properties
	//
	//--------------------------------------------------------------------------

	//----------------------------------
	//  items
	//----------------------------------

	/**
	 *  @private
	 */
	mx_internal function get items():Array
	{
		return _renderData ? _renderData.filteredCache : null;
	}

	//----------------------------------
	//  itemType
	//----------------------------------

	[Inspectable(environment="none")]

	/**
	 *  The subtype of ChartItem used by this series
	 *  to represent individual items. 
	 *  Subclasses can override and return a more specialized class
	 *  if they need to store additional information in the items.
	 */
	protected function get itemType():Class
	{
		return PlotSeriesItem;
	}

	//----------------------------------
	//  radius
	//----------------------------------

    [Inspectable(category="Styles")]
	
	/** 
	 *  Specifies the radius, in pixels, of the chart element
	 *  at each data point. 
	 *  By default, the PlotChart control draws a circle at each data point.  
	 *  You can set this property in MXML or using styles. 
	 *  
	 *  @default 4
	 */
	public function get radius():Number
	{
		return getStyle("radius");
	}
	
	/**
	 *  @private
	 */
	public function set radius(value:Number):void
	{
		setStyle("radius", value);
	}

	//----------------------------------
	//  renderDataType
	//----------------------------------

	[Inspectable(environment="none")]

	/**
	 *  The subtype of ChartRenderData used by this series to store all data necessary to render.
	 *  Subclasses can override and return a more specialized class if they need to store additional information for rendering.
	 */
	protected function get renderDataType():Class
	{
		return PlotSeriesRenderData;
	}

	//----------------------------------
	//  xField
	//----------------------------------

	/**
	 *  @private
	 *  Storage for the xField property.
	 */
	private var _xField:String = "";		
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider
	 *  that determines the x-axis location of each data point. 
	 *  If <code>null</code>, Flex renders the data points
	 *  in the order they appear in the dataProvider. 
	 *  
	 *  @default null
	 */
	public function get xField():String
	{
		return _xField;
	}

	/**
	 *  @private
	 */
	public function set xField(value:String):void
	{
		_xField = value;

		dataChanged();
	}

	//----------------------------------
	//  yField
	//----------------------------------

	/**
	 *  @private
	 *  Storage for the yField property.
	 */
	private var _yField:String = "";	
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider
	 *  that determines the y-axis location of each data point. 
	 *  If <code>null</code>, the PlotSeries assumes the data provider
	 *  is an Array of numbers, and uses the numbers as values. 
	 *  
	 *  @default null
	 */
	public function get yField():String
	{
		return _yField;
	}

	/**
	 *  @private
	 */
	public function set yField(value:String):void
	{
		_yField = value;

		dataChanged();
	}

	//--------------------------------------------------------------------------
	//
	//  Overridden methods: UIComponent
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	override protected function updateDisplayList(unscaledWidth:Number,
												  unscaledHeight:Number):void
	{
		super.updateDisplayList(unscaledWidth, unscaledHeight);
		
		var g:Graphics = graphics;
		g.clear();

		var renderData:PlotSeriesRenderData =
			transitionRenderData ?
			PlotSeriesRenderData(transitionRenderData) :
			_renderData;
		if (!renderData)
			return;
		
		var renderCache:Array = renderData.filteredCache;

		var sampleCount:int = renderCache.length;
		var i:int;
		var instances:Array;
		var inst:IFlexDisplayObject;
		var rc:Rectangle;
		var v:PlotSeriesItem;
		
		_instanceCache.factory = getStyle("itemRenderer");
		_instanceCache.count = sampleCount;			
		instances = _instanceCache.instances;

		var bSetData:Boolean =
			(sampleCount > 0 && (instances[0] is IDataRenderer));

		if (renderData == transitionRenderData &&
			transitionRenderData.elementBounds)
		{
			var elementBounds:Array = renderData.elementBounds;

			for (i = 0; i < sampleCount; i++)
			{
				inst = instances[i];
				v = renderCache[i];
				v.itemRenderer = inst;
				if (bSetData)
					(inst as IDataRenderer).data = v;
				rc = elementBounds[i];
				inst.move(rc.left, rc.top);
				inst.setActualSize(rc.width, rc.height);
			}			
		}
		else
		{
			var radius:Number = getStyle("radius");
						
			for (i = 0; i < sampleCount; i++)
			{
				v = renderCache[i];
				inst = instances[i];
				v.itemRenderer = inst;
				if (bSetData)
					(inst as IDataRenderer).data = v;
				inst.move(v.x - radius, v.y - radius);
				inst.setActualSize(2 * radius, 2 * radius);
			}
		}
	}

	/**
	 *  @private
	 */
	override public function styleChanged(styleProp:String):void
	{
		super.styleChanged(styleProp);

		invalidateDisplayList();
		legendDataChanged();
		invalidateData();
	}

	/**
	 *  @private
	 */
	override public function describeData(dimension:String,
										  requiredFields:uint):Array
	{
		validateData();

		var desc:DataDescription = new DataDescription();
		var cache:Array = _renderData.cache;
		var radius:Number = getStyle("radius");
		
		if (dimension == CartesianTransform.VERTICAL_AXIS)
		{
			if ((requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0)
			{
				cache = cache.concat();
				cache.sortOn("yNumber",Array.NUMERIC);		
			}
			extractMinMax(cache, "yNumber", desc, (requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0);
			if ((requiredFields & DataDescription.REQUIRED_BOUNDED_VALUES) != 0)
			{
				desc.boundedValues= [];
				desc.boundedValues.push(new BoundedValue(desc.max,0,radius));
				desc.boundedValues.push(new BoundedValue(desc.min,radius,0));
			}
		}
		else if (dimension == CartesianTransform.HORIZONTAL_AXIS)
		{
			if ((requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0)
			{
				cache = cache.concat();
				cache.sortOn("xNumber",Array.NUMERIC);		
			}
			extractMinMax(cache, "xNumber", desc, (requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0);
			if ((requiredFields & DataDescription.REQUIRED_BOUNDED_VALUES) != 0)
			{
				desc.boundedValues= [];
				desc.boundedValues.push(new BoundedValue(desc.max,0,radius));
				desc.boundedValues.push(new BoundedValue(desc.min,radius,0));
			}
		}
		else
		{
			return [];
		}

		return [ desc ];	
	}

	/**
	 *  @private
	 */
	override public function findDataPoints(x:Number, y:Number,
											sensitivity:Number):Array
	{
		if (!interactive)
			return [];

		var radius:Number = getStyle("radius");
		var minDist2:Number = radius + sensitivity;
		minDist2 *= minDist2;
		var minItems:Array = [];		 
		var pr2:Number = radius * radius;
		
		var n:int = _renderData.filteredCache.length;
		var i:int;
		
		for (i = n - 1; i >= 0; i--)
		{
			var v:Object= _renderData.filteredCache[i];			
			var dist:Number = (v.x  - x) * (v.x  - x) + (v.y - y) * (v.y - y);
			if (dist <= minDist2)
				minItems.push(v);
		}

		for (i = 0; i < minItems.length; i++)
		{
			var item:PlotSeriesItem = minItems[i];
			var hd:HitData = new HitData(createDataID(item.index),
										 Math.sqrt(minDist2),
										 item.x, item.y, item);
			hd.dataTipFunction = formatDataTip;
			minItems[i] = hd;
		}

		return minItems;
	}

	//--------------------------------------------------------------------------
	//
	//  Overridden methods: Series
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	override protected function updateData():void
	{
		var renderDataType:Class = this.renderDataType;
		_renderData = new renderDataType();

		_renderData.cache = [];
		var i:int = 0;
		var itemType:Class = this.itemType;
		if (cursor)
		{
			cursor.seek(CursorBookmark.FIRST);
			while (!cursor.afterLast)
			{
				_renderData.cache[i] = new itemType(this, cursor.current, i);
				i++;
				cursor.moveNext();
			}
		}

		cacheIndexValues(_xField, _renderData.cache, "xValue");
		cacheDefaultValues(_yField, _renderData.cache, "yValue");

		super.updateData();

		var radius:Number = getStyle("radius");

		var n:int = _renderData.cache.length;
		for (i = 0; i < n; i++)
		{
			_renderData.cache[i].radius = radius;
		}
	}

	/**
	 *  @private
	 */
	override protected function updateMapping():void
	{
		dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).mapCache(
			_renderData.cache, "xValue", "xNumber", (_xField == ""));
		dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).mapCache(
			_renderData.cache, "yValue", "yNumber");

		super.updateMapping();
	}

	/**
	 *  @private
	 */
	override protected function updateFilter():void
	{
		if (filterData)
		{
			_renderData.filteredCache = _renderData.cache.concat();
			
			dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).filterCache(
				_renderData.filteredCache, "xNumber", "xFilter");
			dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).filterCache(
				_renderData.filteredCache, "yNumber", "yFilter");
			
			stripNaNs(_renderData.filteredCache, "yFilter");
			stripNaNs(_renderData.filteredCache, "xFilter");
		}
		else
		{
			_renderData.filteredCache = _renderData.cache;
		}

		super.updateFilter();
	}
	
	/**
	 *  @private
	 */
	override protected function updateTransform():void
	{
		dataTransform.transformCache(
			_renderData.filteredCache, "xNumber", "x", "yNumber", "y");
		
		super.updateTransform();
	}

	/**
	 *  @private
	 */
	override public function getElementBounds(renderData:Object):void
	{
		var cache:Array = renderData.cache;
		var rb:Array = [];
		var sampleCount:int = cache.length;		

		if (sampleCount)
		{
			var radius:Number = renderData.radius;
			var v:Object = cache[0];
			var maxBounds:Rectangle = new Rectangle(v.x, v.x, 0, 0);
	
			for (var i:int = 0; i < sampleCount; i++)
			{
				v = cache[i];
				
				var b:Rectangle = new Rectangle(v.x - radius, v.y - radius,
												2 * radius, 2 * radius);
				
				maxBounds.left = Math.min(maxBounds.left, b.left);
				maxBounds.top = Math.min(maxBounds.top, b.top);
				maxBounds.right = Math.max(maxBounds.right, b.right);
				maxBounds.bottom = Math.max(maxBounds.bottom, b.bottom);
				
				rb[i] = b;
			}
		}
		else
		{
			maxBounds = new Rectangle();
		}

		renderData.elementBounds = rb;
		renderData.bounds =  maxBounds;
	}

	/**
	 *  @private
	 */
	override public function beginInterpolation(sourceRenderData:Object,
												destRenderData:Object):Object
	{
		var idata:Object = initializeInterpolationData(
			sourceRenderData.cache, destRenderData.cache,
			{ x: true, y: true }, itemType);
		
		var interpolationRenderData:PlotSeriesRenderData =
			PlotSeriesRenderData(destRenderData.clone());

		interpolationRenderData.cache = idata.cache;	
		interpolationRenderData.filteredCache = idata.cache;	

		transitionRenderData = interpolationRenderData;
		return idata;
	}

	/**
	 *  @private
	 */
	override protected function getMissingInterpolationValues(
									sourceProps:Object, srcCache:Array,
									destProps:Object, destCache:Array,
									index:Number, customData:Object):void
	{
		for (var propName:String in sourceProps)
		{
			var src:Number = sourceProps[propName];
			var dst:Number = destProps[propName];

			if (propName == "x" || propName == "y")	
			{
				if (isNaN(src))
					src = dst;
				if (isNaN(dst))
					dst = src;
			}

			sourceProps[propName] = src;
			destProps[propName] = dst;
		}		
	}

	//--------------------------------------------------------------------------
	//
	//  Methods
	//
	//--------------------------------------------------------------------------

	/**
	 *  Customizes the item renderer instances that are used to represent the chart. This method is called automatically
	 *	whenever a new item renderer is needed while the chart is being rendered. You can override this method to add your own customization as necessary.
	 *	@param	instance	The new item renderer instance that is being created.
	 *	@param	cache		The InstanceCache that is used to manage the item renderer instances.
	 */
	protected function applyItemRendererProperties(instance:DisplayObject,
											   cache:InstanceCache):void
	{
		if (instance is ISimpleStyleClient)
			ISimpleStyleClient(instance).styleName = this;
	}

	/**
	 *  @private
	 */
	private function formatDataTip(hd:HitData):String
	{
		var dt:String = "";
		var n:String = displayName;
		if (n && n != "")
			dt += "<b>" + n + "</b><BR/>";

		var xName:String = dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).displayName;
		if (xName != "")
			dt += "<i>" + xName+ ":</i> ";
		dt += dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).formatForScreen(
			PlotSeriesItem(hd.chartItem).xValue) + "\n";

		var yName:String = dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).displayName;
		if (yName != "")
			dt += "<i>" + yName + ":</i> ";
		dt += dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).formatForScreen(
			PlotSeriesItem(hd.chartItem).yValue) + "\n";
			
		return dt;
	}
}

}
